package pjaws.paw;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import pjaws.PjawsConstants;
/*
 *   A Paw is an abstract class to describe each node with in a cluster.  
 *   
 *   Each Paw must handle processing of data sent and received from other paws (nodes). 
 *   
 *   @author Zachary Cancio
 */

public abstract class Paw extends Thread {
	private int myPawId;
	private String myRootHostname;
	private int myRootPort;
	private Socket myRootSocket;
	private DataInputStream myRootInStream;
	private DataOutputStream myRootOutStream;
	private Map<Integer, PawSocket> myPawSockets;
	private Map<Integer, Socket> myReceiveSockets;
	private Map<Integer, Socket> mySendSockets;
	private PawServerSocket myPawSS;

	// flags
	private boolean PawIdSet = false;
	private boolean RootHostnameAndPortSet = false;

	// --------CONSTRUCTORS------//
	/**
	 * Main Constructor used when initialization within a pjaws application
	 */
	public Paw() {
		initialize();
	}
	
	public Paw(int pawId) {
		myPawId = pawId;
		initialize();
	}
	
	

	/**
	 * Used by the a RemotePaw program to create the Paw form an initialization
	 * File
	 * 
	 * @param filename
	 *            the initialization file
	 */
	public Paw(String filename) {
		initialize();
		initFromFile(filename);

	}

	// -------STATIC METHODS--------//
	public static String getInputString(InputStream in, int length)
			throws IOException {
		byte[] byteStr = new byte[length];
		in.read(byteStr);
		return new String(byteStr);
	}

	// --------INITIALIZATION METHODS------//
	private void initialize() {
		myPawSockets = new HashMap<Integer, PawSocket>();
		// myReceivePawSockets = new HashMap<Integer, PawSocket>();
		myReceiveSockets = new HashMap<Integer, Socket>();
		// mySendPawSockets = new HashMap<Integer, PawSocket>();
		mySendSockets = new HashMap<Integer, Socket>();

	}

	public void initFromFile(String filename) {
		try {
			FileReader fr = new FileReader(filename);
			BufferedReader br = new BufferedReader(fr);
			String currLine;
			while ((currLine = br.readLine()) != null) {
				System.out.println("PROCESSSING: " + currLine);
				String[] values = currLine.split("\\s+");
				if (values[0].equals("ID")) {
					int pawId = Integer.parseInt(values[1]);
					setPawId(pawId);
				} else if (values[0].equals("ROOT")) {
					String hostname = values[1];
					int port = Integer.parseInt(values[2]);
					this.setRootHostnameAndPort(hostname, port);

				} else if (values[0].equals("SKT")) {
					// get the paw id of the paw on the other end of the socket
					int otherPawId = Integer.parseInt(values[1]);
					// get the hostname of the other paw
					String hostname = values[2];
					// get the port of the other paw
					int port = Integer.parseInt(values[3]);
					// create the paw message
					PawSocket ps = new PawSocket(otherPawId, hostname, port);
					addPawSocket(ps);

				}
			}
			br.close();
			fr.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Add a PaW socket to its respective map based on the direction.
	 * 
	 * @param ps
	 *            the new paw socket to be added
	 * @param d
	 *            the direction of the new socket
	 */
	public void addPawSocket(PawSocket ps) {
		myPawSockets.put(ps.getOtherPawId(), ps);
	}

	// --------- SETUP METHODS ------- //
	/**
	 * Setup is the method that establishes all the sockets once a PaW is ready
	 * to run. This is a helper method that should only be called when the PaW
	 * is ready to run.
	 * <p>
	 * 1) Every PaW has a PawServerSocket to wait for socket requests. Socket
	 * requests it receives are designated as receive sockets. Receive sockets
	 * are added to a Map that matches the PawId of the requesting PaW to the
	 * accepted socket.
	 * <p>
	 * 2) Once the PawServerSocket is running, the PaW notifies the root. The
	 * PaW only continues setting up once it receives a "PROCEED" from the root.
	 * <p>
	 * 3) The PaW then setups its send sockets. These are sockets used only for
	 * sending. The sockets are added to another Map where the key is the PawId
	 * of the PaW on the other end of the socket. The PaW only continues once it
	 * receives a "PROCEED" from the root.
	 * 
	 */
	private void setup() {
		// Step 1
		startPawServerSocket();

		try {
			connectToRoot();
			System.out.println("Connected to Root");

		} catch (UnknownHostException e) {
			System.out.println("Failed to connect to Root");
			e.printStackTrace();
		} catch (IOException e) {
			System.out.println("Failed to establish IO with Root");
			e.printStackTrace();
		}
		// Step 2
		sendReadyToRootAndWait();
		System.out.println("Ready to Receive");

		// Step 3
		setupSendSockets();
		System.out.println("Send sockets established");
		sendReadyToRootAndWait();
		System.out.println("Ready to Send");

	}

	/**
	 * Start a PawServerSocket thread to listen for new socket connections on
	 * PaW communication port.
	 */
	public void startPawServerSocket() {
		try {
			myPawSS = new PawServerSocket(this);
			myPawSS.start();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Adds a receive socket to the map of all sockets. This method is called by
	 * the PawServerSocket every time a socket is accepted.
	 * 
	 * @param PawId
	 *            The id of the paw on the other end of the socket connection.
	 * @param s
	 *            The socket to be added.
	 */
	public synchronized void addReceiveSocket(int PawId, Socket s) {
		// verify that the pawid exists.
		if (myPawSockets.containsKey(PawId)) {
			myReceiveSockets.put(PawId, s);
			System.out.println("Added Receive socket connected to " + PawId);

		}
	}

	/**
	 * Procedural method to connect to the Root and create input/output streams
	 * 
	 * @throws IOException
	 * @throws UnknownHostException
	 */
	public void connectToRoot() throws UnknownHostException, IOException {

		myRootSocket = new Socket(myRootHostname, myRootPort);
		BufferedInputStream bis = new BufferedInputStream(myRootSocket
				.getInputStream());
		myRootInStream = new DataInputStream(bis);
		myRootOutStream = new DataOutputStream(myRootSocket.getOutputStream());

	}

	/**
	 * Sends ready message to server that all ServerSockets are waiting to
	 * accept connections. Waits for proceed message before trying to establish
	 * socket connections.
	 * 
	 */
	public void sendReadyToRootAndWait() {

		boolean proceed = false;
		try {
			// tell the root my id
			myRootOutStream.write(PjawsConstants.PAWID.getBytes());
			myRootOutStream.writeInt(myPawId);
			// tell the root i'm ready
			myRootOutStream.write(PjawsConstants.STATUS.getBytes());
			myRootOutStream.writeInt(PjawsConstants.READY.length());
			myRootOutStream.write(PjawsConstants.READY.getBytes());
			// wait for proceed command before establishing sockets
			byte[] cmdData = new byte[PjawsConstants.CMDLEN];
			while (!proceed) {
				myRootInStream.read(cmdData);
				String cmd = new String(cmdData);
				if (cmd.equals(PjawsConstants.STATUS)) {
					int length = myRootInStream.readInt();
					byte[] data = new byte[length];
					myRootInStream.read(data);
					String status = new String(data);
					if (status.equals(PjawsConstants.PROCEED)) {
						proceed = true;
					}
				}
			}
			System.out.println("proceeding");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * For each connection where a Paw will send a message, a separate send
	 * socket is created. The send socket is added to the send-socket map.
	 */
	public void setupSendSockets() {
		for (PawSocket ps : myPawSockets.values()) {
			try {
				Socket s = new Socket(ps.getHostname(), ps.getPort());
				// send paw id
				DataOutputStream dos = new DataOutputStream(s.getOutputStream());
				dos.write(PjawsConstants.PAWID.getBytes());
				dos.writeInt(this.myPawId);
				mySendSockets.put(ps.getOtherPawId(), s);
				System.out.println("Added Send Socket connected to "
						+ ps.getOtherPawId());
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}

	}

	// -------COMMUNICATION METHODS------//
	/**
	 * This method attempts to send a data packet to a paw. Whether the socket
	 * is a regular socket or a server socket, send abstracts the differences.
	 * <p>
	 * The method first grabs the paw message object corresponding to that
	 * message id. From the message object, it gets the paw id of the paw the
	 * message will be sent to. It then sends a packet according to the
	 * specification described below.
	 * <p>
	 * [STARTCMD][PMID][DATA LENGTH][DATA]
	 * <p>
	 * STARTCMD in this case is data. [4 char string]
	 * <p>
	 * PMID is the Paw Message Id [int]
	 * <p>
	 * DATA LENGTH the length of the data in bytes [long] DATA all the data you
	 * want to send [byte(s)]
	 * <p>
	 * NOTE: This method is blocking. Until the method receives an "ACCEPT" from
	 * the receiving Paw, it will continue attempting to send the message.
	 * 
	 * @param data
	 *            A byte array of all the data to be sent.
	 * @param length
	 *            The length of the data packet
	 * @param PawId
	 *            The Paw Id of the Paw receiving this message
	 * @param pmid
	 *            The paw message id
	 * 
	 */
	public synchronized void sendPawMessage(Object o, int PawId, int pmId)
			throws IOException {
		DataInputStream inStream;
		String status = PjawsConstants.READY;
		// continue retrying sending message until READY status received
		// do {
		if (myReceiveSockets.containsKey(PawId)) {
			System.out.println("I have a send socket for " + PawId);
		}
		// get the socket thats connected to the paw I want
		Socket socket = mySendSockets.get(PawId);
		// setup output stream
		DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
		ObjectOutputStream outStream = new ObjectOutputStream(dos);

		// set up input stream
		BufferedInputStream bis = new BufferedInputStream(socket
				.getInputStream());
		inStream = new DataInputStream(bis);

		// send STARTCMD
		outStream.write(PjawsConstants.DATA.getBytes()); // DATA
		System.out.println("STARTCMD sent ");

		// send paw message id
		outStream.writeInt(pmId);
		System.out.println("paw message id sent ");

		// send object
		outStream.writeObject(o);
		System.out.println("object sent ");

		String cmd = getInputString(inStream, PjawsConstants.CMDLEN);
		System.out.println("got cmd: " + cmd);

		if (cmd.equals(PjawsConstants.STATUS)) {
			int len = inStream.readInt();
			System.out.println("got len: " + len);
			status = getInputString(inStream, len);
			System.out.println("got status: " + status);

		} else {
			// TODO throw an error?
			// break;
		}
		// } while (status.equals(PjawsConstants.BUSY));

	}

	/**
	 * Receive data from the socket that matches the pmId. Once data is received
	 * 
	 * @param PawId
	 *            the paw id that the message is received from
	 * @param pmId
	 *            the paw message id of the message received.
	 * 
	 * 
	 * @return byte array of all the data. the beginning of the data should have
	 *         the.
	 * @throws IOException
	 * @throws ClassNotFoundException
	 * 
	 */
	public synchronized Object receivePawMessage(int PawId, int pmId)
			throws IOException, ClassNotFoundException {
		;

		// make sure I have a socket for this paw
		if (!myReceiveSockets.containsKey(PawId)) {
			return null;
		}
		Socket socket = myReceiveSockets.get(PawId);
		// setup output stream
		DataOutputStream outStream = new DataOutputStream(socket
				.getOutputStream());

		// setup input Stream
		BufferedInputStream bis = new BufferedInputStream(socket
				.getInputStream());
		ObjectInputStream inStream = new ObjectInputStream(bis);

		// read STARTCMD
		String startcmd = getInputString(inStream, PjawsConstants.CMDLEN);
		if (!startcmd.equals(PjawsConstants.DATA)) {
			return null;
		}
		System.out.println("got cmd: " + startcmd);

		// read paw message id
		int recvPmId = inStream.readInt();
		if (recvPmId != pmId) {
			return null;
		}
		System.out.println("got paw message id: " + pmId);

		// read object
		Object o = inStream.readObject();
		System.out.println("got object: " + o);

		// send status as ready
		outStream.write(PjawsConstants.STATUS.getBytes());
		outStream.writeInt(PjawsConstants.STATUS.getBytes().length);
		outStream.write(PjawsConstants.READY.getBytes());

		return o;
	}

	// ------- ACCESS METHODS ------//
	/**
	 * Set this PaW's Id.
	 * 
	 * @param pawId
	 *            the unique id
	 */
	public void setPawId(int pawId) {
		myPawId = pawId;
		PawIdSet = true;
	}

	public int getPawId() {
		return myPawId;
	}

	/**
	 * This method sets the root hostname and port. This is useful when
	 * generating the parallel code as the Pjaws application will take care of
	 * setting these variables.
	 * 
	 * 
	 * @param hostname
	 *            the hostname of the root
	 * @param port
	 *            the port on which to connect
	 */
	public void setRootHostnameAndPort(String hostname, int port) {
		myRootHostname = hostname;
		myRootPort = port;
		RootHostnameAndPortSet = true;
	}

	public String getRootHostname() {
		return myRootHostname;
	}

	public int getRootPort() {
		return myRootPort;
	}

	/**
	 * TODO another code smell?
	 * 
	 * @return Map of the PawSockets
	 */

	public Map<Integer, PawSocket> getPawSockets() {
		return myPawSockets;
	}



	// ------ABSTRACT METHODS------//
	public abstract void run();

	// ----- EXECUTION METHODS -----//
	/**
	 * overrides Tread class start TODO check if this breaks
	 */
	public void start() {
		// ensure the root socket information is set
		// ensure that this paw has an Id
		if (RootHostnameAndPortSet && PawIdSet) {
			setup();
			run();
			end();

		}
	}

	public void simulate() {
		// change instance to just local ports.
	}

	public void end(){
		System.out.println("exiting");
		//TODO clean up!!!
		System.exit(1);
		
	}
	
	/**
	 * Creates the initialization file for this PaW.
	 * 
	 * @param filename
	 *            The filename (includes filepath) where the initialization file
	 *            should be saved.
	 */

	public void generateInitFile(String filename) {
		try {
			FileWriter fr = new FileWriter(filename);

			// write paw id
			String idLine = "ID\t" + myPawId + "\n";
			fr.write(idLine);
			// write root information
			String rootLine = "ROOT\t" + myRootHostname + "\t" + myRootPort
					+ "\n";
			fr.write(rootLine);
			// write socket information
			// e.g. SKT 93 teer13.oit.duke.edu 9800 //receive initial
			// data

			for (PawSocket ps : myPawSockets.values()) {
				String recvPawSocketLine = "SKT\t" + ps.getOtherPawId() + "\t"
						+ ps.getHostname() + "\t" + ps.getPort() + "\n";
				fr.write(recvPawSocketLine);
			}

			fr.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Write the paw to file using writeObject.
	 * 
	 * @param filename
	 *            where the paw will be saved
	 */

	public void writeThisPawToFile(String filename) {
		if (!filename.endsWith(".paw")) {
			// throw error
		}

		try {
			FileOutputStream fos = new FileOutputStream(filename);
			ObjectOutputStream out = new ObjectOutputStream(fos);
			out.writeObject(this);
			out.close();
			fos.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public String getClassFilepath(){
		String name = getClass().getCanonicalName().replace(".", "$");
		return getClass().getProtectionDomain()
		.getCodeSource().getLocation().getFile() + name + ".class";
	}
	
	

}
